package com.unknownman.block.transaction.subscribe;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.unknownman.block.component.MessageSolverChooser;
import com.unknownman.block.constants.ChainContractConstants;
import com.unknownman.block.constants.ChainContractConstants.ContractTypeConstants;
import com.unknownman.block.constants.LogConstants;
import com.unknownman.block.dto.ChainContractComposition;
import com.unknownman.block.entity.TContractConfig;
import com.unknownman.block.entity.TContractLogs;
import com.unknownman.block.service.TChainConfigService;
import com.unknownman.block.service.TContractConfigService;
import com.unknownman.block.service.TContractLogsService;
import com.unknownman.block.utils.HttpClientUtil;
import lombok.extern.slf4j.Slf4j;
import org.cloud.exception.BusinessException;
import org.cloud.utils.CollectionUtil;
import org.cloud.utils.CommonUtil;
import org.cloud.utils.SpringContextUtil;
import org.springframework.data.util.Pair;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.web3j.contracts.eip20.generated.ERC20;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.ReadonlyTransactionManager;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.utils.Numeric;

import java.math.BigInteger;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Slf4j
public class EthTranFilter3 extends BaseTranFilter {

    private String bscScanUrl = "https://api.bscscan.com/api";

    private boolean stop = false;
    private long checkInterval = 2000;

    private final MessageSolverChooser messageSolverChooser;

    private final TChainConfigService chainConfigService;
    private final TContractLogsService contractLogsService;
    private final TContractConfigService contractConfigService;

    private final ChainContractComposition chainContractComposition;

    private final Map<String, TContractConfig> contractConfigMap;

    public EthTranFilter3(ChainContractComposition chainContractComposition) {
        super();
        //配置中读取apikey
        String apikey = CommonUtil.single().getEnv("config.bscscan.apikey", "");
        if (!StringUtils.isEmpty(apikey)) {
            bscScanUrl = bscScanUrl + "?apikey=" + apikey;
        }
        chainConfigService = SpringContextUtil.getBean(TChainConfigService.class);
        contractLogsService = SpringContextUtil.getBean(TContractLogsService.class);
        messageSolverChooser = SpringContextUtil.getBean(MessageSolverChooser.class);
        contractConfigService = SpringContextUtil.getBean(TContractConfigService.class);
        this.chainContractComposition = chainContractComposition;
        //查出启用的合约地址
        contractConfigMap = this.chainContractComposition.getContractList().stream()
            .filter(cont -> cont.getContractStatus() == ChainContractConstants.ContractStatusConstants.NORMAL.getStatus())
            .collect(Collectors.toMap((contractConfig) -> contractConfig.getContractAddress().toUpperCase(), Function.identity()));

        this.createWe3j();
    }

    /**
     * 添加监听
     */
    @Override
    public boolean addFilter(BigInteger startBlock) {
        lastBlock = startBlock;
        stop = false;
        log.info("addFilter3: 开始扫描区块获取事件, [startBlock: {}]", startBlock);
        new Thread(() -> {
            long nextCheck = 0;
            while (!(Thread.interrupted() || stop)) {
                if (nextCheck <= System.currentTimeMillis()) {
                    nextCheck = System.currentTimeMillis() + checkInterval;
                    try {
                        runScan();
                    } catch (Exception e) {
                        stop = true;
                        log.error("run scan error::[{}]", e.getMessage());
                    }
                } else {
                    try {
                        Thread.sleep(Math.max(nextCheck - System.currentTimeMillis(), 500));
                    } catch (InterruptedException ex) {
                        log.error("addFilter3: 扫描区块获取事件异常, [lastBlock: {}, e: {}]", lastBlock, ex.getMessage());
                        break;
                    }
                }
            }
            log.warn("addFilter3: 结束扫描区块获取事件, [lastBlock: {}]", lastBlock);
        }).start();
        return true;
    }

    private Pair<Long, JSONArray> callBscScan(String contract, long start, long end) throws Exception {
        String callResult = HttpClientUtil.getInstance().sendHttpGet(getLogsUrl(contract, start, end));
        JSONObject callResultJson = JSONObject.parseObject(callResult);
        if (!"1".equals(callResultJson.getString("status"))) {
            if ("No records found".equalsIgnoreCase(callResultJson.getString("message"))) {
                log.info("【{}】到【{}】无【{}】的交易", start, end, contract);
                return Pair.of(end, new JSONArray());
            }
            throw new BusinessException(
                "call bscscan响应状态异常, status: " + callResultJson.getString("status") + "; result: " + callResultJson.getString("result"));
        }
        JSONArray resultArr = callResultJson.getJSONArray("result");
        if (resultArr.size() >= 1000) {
            Assert.isTrue(end > start, "end 必须小于 start");
            end = (end - start) / 2 + start;
            return callBscScan(contract, start, end);
        }
        return Pair.of(end, resultArr);
    }


    private void runScan() throws Exception {
        long currentBlockNumber = lastBlock.longValue();
        long startBlock = currentBlockNumber + 1;
        BigInteger latest = getLatestBlockNumber();
        Assert.isTrue(latest.compareTo(BigInteger.ZERO) > 0, "获取最新区块错误!!");
        if (latest.longValue() <= currentBlockNumber) {
            return;
        }
        long endBlock = latest.longValue() - startBlock > 2000 ? startBlock + 2000 : latest.longValue();
        JSONArray logs = new JSONArray();
        for (String contractAddress : contractConfigMap.keySet()) {
            try {
                Pair<Long, JSONArray> longJSONArrayPair = callBscScan(contractAddress.toLowerCase(Locale.ROOT), startBlock, endBlock);
                //重新记录实际查询数据使用的end
                endBlock = longJSONArrayPair.getFirst();
                JSONArray resultArr = longJSONArrayPair.getSecond();
                if (CollectionUtil.single().isEmpty(resultArr)) {
                    continue;
                }
                logs.addAll(resultArr);
                Thread.sleep(500);
            } catch (Exception e) {
                log.error("获取区块数据异常,{}", e.getMessage());
                return;
            }
        }
        if (CollectionUtil.single().isNotEmpty(logs)) {
            subscribeCallBack(logs);
        }

        // 回调扫块步长的1/2，防止掉块
        if (endBlock >= latest.longValue()) {
            lastBlock = BigInteger.valueOf(startBlock - 1);
        } else if (endBlock < startBlock + 3) {  // 正常情况下这里应该不会执行
            lastBlock = BigInteger.valueOf(endBlock);
        } else {
            lastBlock = BigInteger.valueOf(startBlock + (endBlock - startBlock) / 2);
        }
        updateLastBlock(lastBlock, true);
    }


    @Override
    public void subscribeCallBack(TransactionReceipt transactionReceipt) throws Exception {
        throw new RuntimeException("当前方法不允许调用");
    }

    public void subscribeCallBack(JSONArray logs) throws Exception {
        if (CollectionUtil.single().isEmpty(logs)) {
            return;
        }
        ArrayList<TContractLogs> saveLogList = new ArrayList<>();
        for (Object logObject : logs) {
            JSONObject log = JSONObject.parseObject(logObject.toString());
            String contractAddress = log.getString("address");
            //判断是不是监听的日志
            TContractConfig contractConfig = contractConfigMap.get(contractAddress.toUpperCase());
            if (contractConfig == null) {
                continue;
            }
            String transactionHash = log.getString("transactionHash");
            String logIndexStr = log.getString("logIndex");
            logIndexStr = logIndexStr.equals("0x") ? "0x0" : logIndexStr;
            Long logIndex = Numeric.decodeQuantity(logIndexStr).longValue();
            if (!contractLogsService.existsByHashAndIndex(transactionHash, logIndex)) {
                Long timeStamp = Numeric.decodeQuantity(log.getString("timeStamp")).longValue();
                JSONArray topics = log.getJSONArray("topics");
                String data = log.getString("data");
                data = data.equals("0x") ? "0x0" : data;
                TContractLogs contractLogs = new TContractLogs();
                contractLogs.setContractConfigId(contractConfig.getContractConfigId());
                contractLogs.setContractAddress(contractAddress);
//                contractLogs.setTransFrom(transactionReceipt.getFrom());
//                if (!StringUtils.isEmpty(transactionReceipt.getContractAddress())) {
//                    contractLogs.setTransTo(transactionReceipt.getContractAddress());
//                } else {
//                    contractLogs.setTransTo(transactionReceipt.getTo());
//                }
                contractLogs.setTransHash(transactionHash);
                contractLogs.setIndexForBlock(logIndex);
                contractLogs.setBlockTimestamp(new Date(timeStamp * 1000));
                contractLogs.setRpcAddress(chainContractComposition.getRpcAddress());

                contractLogs.setFirstTopic(topics.get(0).toString());
                if (topics.size() >= 2) {
                    contractLogs.setSecondTopic(topics.get(1).toString());
                }
                if (topics.size() >= 3) {
                    contractLogs.setThirdTopic(topics.get(2).toString());
                }
                if (topics.size() >= 4) {
                    contractLogs.setFourthTopic(topics.get(3).toString());
                }
                contractLogs.setLogData(data);
                contractLogs.setStatus(LogConstants.ContractLogStatus.UNTREATED.getStatus());
                saveLogList.add(contractLogs);
                //消息发送
                //根据合约分发消息
//                MessageSolver messageSolver = messageSolverChooser.choose(logChain.getAddress().toUpperCase());
//                if (messageSolver != null) {
//                    messageSolver.solve(logChain);
//                }
            }
        }
        if (CollectionUtil.single().isNotEmpty(saveLogList)) {
            boolean b = contractLogsService.saveBatch(saveLogList);
            Assert.isTrue(b, "subscribeCallBack-" + chainContractComposition.getChainSymbol() + "：log保存失败");
        }

    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateLastBlock(BigInteger block, boolean success) {
        chainConfigService.updateLastBlock(chainContractComposition.getChainConfigId(), block.longValue(), success);
    }

    /**
     * 订阅是否关闭
     *
     * @return
     */
    @Override
    public boolean isDisposed() {
        return stop;
    }

    /**
     * 关闭订阅
     */
    @Override
    public void stopSubscribe() {
        stop = true;
    }

    private void createWe3j() {
        final String[] bscRpcList = {
            "https://bsc-dataseed.binance.org",
            "https://bsc-dataseed1.binance.org",
            "https://bsc-dataseed2.binance.org",
            "https://bsc-dataseed3.binance.org",
            "https://bsc-dataseed4.binance.org",
            "https://bsc-dataseed1.defibit.io",
            "https://bsc-dataseed2.defibit.io",
            "https://bsc-dataseed3.defibit.io",
            "https://bsc-dataseed4.defibit.io",
            "https://bsc-dataseed1.ninicoin.io",
            "https://bsc-dataseed2.ninicoin.io",
            "https://bsc-dataseed3.ninicoin.io",
            "https://bsc-dataseed4.ninicoin.io",
            "wss://bsc-ws-node.nariox.org"
        };

        if (!ChainContractConstants.ChainSymbolEnum.BSC.getSymbol().equalsIgnoreCase(chainContractComposition.getChainSymbol())) {
            rpcAddress = chainContractComposition.getRpcAddress();
        } else {
            chainContractComposition.setRpcAddress(bscRpcList[new Random().nextInt(bscRpcList.length)]);
        }
        try {
            web3j = Web3j.build(new HttpService(chainContractComposition.getRpcAddress()));
        } catch (Exception e) {
            log.info("初始化w3[{}]异常，请检查:{}", chainContractComposition.getRpcAddress(), e.getMessage());
            return;
        }
        this.rpcAddress = chainContractComposition.getRpcAddress();
        log.info("初始化w3[{}]成功！", chainContractComposition.getRpcAddress());

        ReadonlyTransactionManager transactionManager = new ReadonlyTransactionManager(web3j, "0x0000000000000000000000000000000000000000");
        this.chainContractComposition.getContractList().forEach((contract) -> {
            if (ContractTypeConstants.ERC20.getType() == contract.getContractType() ||
                ContractTypeConstants.ERC721.getType() == contract.getContractType()
            ) {
                ERC20 erc20 = ERC20.load(contract.getContractAddress(), web3j, transactionManager, new DefaultGasProvider());
                try {
                    if (ContractTypeConstants.ERC721.getType() == contract.getContractType()) {
                        contract.setErc20Decimals(0);
                    } else {
                        contract.setTotalSupply(erc20.totalSupply().send());
                    }
                    contract.setSymbol(erc20.symbol().send());
                    contract.setName(erc20.name().send());
                    contract.setUpdateBy("System");
                    contractConfigService.updateById(contract);
                } catch (Exception e) {
                    log.error("{}", e.getMessage());
                }
            }
        });
    }

    private BigInteger getLatestBlockNumber() {
        String s = HttpClientUtil.getInstance().sendHttpGet(getBlockNumberUrl());
        if (!StringUtils.isEmpty(s)) {
            JSONObject callRsJson = JSONObject.parseObject(s);
            String result = callRsJson.getString("result");
            if (!StringUtils.isEmpty(result)) {
                try {
                    return Numeric.decodeQuantity(result);
                } catch (Exception e) {
                    log.error("call bscscan获取区块高度失败，响应：{},错误信息::{}", callRsJson, e.getMessage());
                }
            }
        }
        return BigInteger.ZERO;
    }

    private String getLogsUrl(String contractAddress, long startBlock, long endBlock) {
        return bscScanUrl + "&module=logs&action=getLogs" + "&address=" + contractAddress + "&fromBlock=" + startBlock + "&toBlock="
            + endBlock;
    }

    private String getBlockNumberUrl() {
        return bscScanUrl + "&module=proxy&action=eth_blockNumber";
    }

}
